using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using Microsoft.SqlServer.Dts.Runtime;

namespace SsisUtilities.PacMan.Components
{
    /// <summary>
    /// This class encapsulates access to the custom XML configuration 
    /// functionality I'm working on to work around context-specific limitations
    /// in the built-in configuration feature. This is likely not to be of 
    /// value to the larger SSIS community, at least not until the whole piece
    /// is done and polished...
    /// </summary>
    public class XmlConfigHelper
    {
        private const string DEFAULT_CONFIG_SUBSET_NAME = "*";

        string VAR_PROJECT_NAME = "User::ProjectName";
        string VAR_PACKAGE_NAME = "System::PackageName";
        string VAR_CURRENT_SITE = "User::CurrentSite";
        string VAR_CURRENT_PROC = "User::CurrentBusinessProcess";
        string SITE_NEUTRAL_PKG = "SiteNeutral";

        private XmlDocument configDoc;
        private Dictionary<string, string> variablesToConfigure  = new Dictionary<string,string>();

        #region Constructors

        /// <summary>
        /// Create a new XmlConfigHelper instance that wraps the supplied custom
        /// XML configuration file
        /// </summary>
        /// <param name="fileName"></param>
        /// <remarks>
        /// If the supplied file does not exist or does not contain the required
        /// elements and attributes, exceptions will occur.
        /// </remarks>
        public XmlConfigHelper(string xmlConfigFileName)
        {
            LoadXmlDoc(xmlConfigFileName);
        }

        /// <summary>
        /// Default constructor
        /// </summary>
        public XmlConfigHelper()
        {
            // Nothing to see here!
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Return all Configuration Subsets in the current custom XML 
        /// configuration file
        /// </summary>
        public List<XmlElement> ConfigurationSubsets
        {
            get
            {
                List<XmlElement> configSubsets = new List<XmlElement>(10);

                foreach (XmlElement configSubset in configDoc.DocumentElement.SelectNodes("configurationSubSet"))
                {
                    configSubsets.Add(configSubset);
                }

                return configSubsets;
            }
        }

        /// <summary>
        /// Return all Configuration Subsets in the current custom XML 
        /// configuration file
        /// </summary>
        public List<string> ConfigurationSubsetNames
        {
            get
            {
                List<string> configSubsetNames = new List<string>(10);

                foreach (XmlElement configSubset in configDoc.DocumentElement.SelectNodes("configurationSubSet"))
                {
                    configSubsetNames.Add(configSubset.Attributes["name"].Value);
                }

                return configSubsetNames;
            }
        }

        /// <summary>
        /// Perform the real work of the XML configuration, applying the values
        /// from the XML file to the SSIS package
        /// </summary>
        public void ConfigureFromXmlStore(string configurationSubsetName, Package package)
        {
            //  Get the variables we need to establish context
            try
            {
                string projectName = ((string)(package.Variables[VAR_PROJECT_NAME].Value));
                string currentSite = ((string)(package.Variables[VAR_CURRENT_SITE].Value));
                string packageName = ((string)(package.Variables[VAR_PACKAGE_NAME].Value));
                string businessProcess = ((string)(package.Variables[VAR_CURRENT_PROC].Value));
                string sourceName = packageName;

                if ((packageName == sourceName))
                {
                    //  Because this script code is in a package-level event handler, it will be executed
                    //  each time that event is fired for any task or container in the package. Go figure.
                    //  Anyway, this packageName = sourcename sanity check will ensure that we only run
                    //  our configuration code when the event source is the package itself and not a child 
                    //  Task.
                    //  Load file into DOM
                    //  Loop over configuration subsets, looking for matches with current site 
                    //  Loop over configuration items, looking for matches with current project and package
                    //  Add matching variables to collection
                    //  Attempt to set variables that match

                    XmlElement rootElement = configDoc.DocumentElement;
                    //  If we're in the CIA master package, or in a package that is not 
                    //  "site aware," such as non-ECM sites, the current site will be 
                    //  "SiteNeutral" and we can continue; otherwise we need to look in 
                    //  the DW database for the current site based on the runtime 
                    //  processing of the CIA master package.
                    if ((currentSite != SITE_NEUTRAL_PKG))
                    {
                        currentSite = configurationSubsetName;
                        //vars[VAR_CURRENT_SITE].Value = currentSite;
                    }
                    AddVariableToConfigDictionary("ConnectionStringDataWarehouse", GetRequiredAttributeString(rootElement, "siteDatabaseConnectionString", packageName));
                    AddVariableToConfigDictionary("DeploymentFolderAbsolutePath", GetRequiredAttributeString(rootElement, "deploymentFolderAbsolutePath", packageName));
                    //  First, configure the default site settings so they can be overwritten later on if necessary - this must be first
                    XmlElement defaultConfigSubsetElement = ((XmlElement)(rootElement.SelectSingleNode("configurationSubSet[@name=\'*\']")));
                    GetSiteVariableValues(defaultConfigSubsetElement, packageName, currentSite, projectName);
                    foreach (XmlElement configSubsetElement in rootElement.SelectNodes("configurationSubSet"))
                    {
                        //  Do not re-configure the default site
                        if ((configSubsetElement.Attributes["name"].Value != "*"))
                        {
                            GetSiteVariableValues(configSubsetElement, packageName, currentSite, projectName);
                        }
                    }
                    //  Now that we have a discrete set of variable names and values, actually update the package
                    foreach (KeyValuePair<string, string> variablePair in variablesToConfigure)
                    {
                        SetVariableValue(variablePair.Key, variablePair.Value, package);
                    }
                }

            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        #region GetConfiguredVariableValues Overloads

        /// <summary>
        /// This overload assumes that the XML config file has already been loaded
        /// </summary>
        public Dictionary<string, string> GetConfiguredVariableValues(Package package, 
            string configurationSubsetName)
        {
            //do work here
            ConfigureFromXmlStore(configurationSubsetName, package);

            return variablesToConfigure;
        }

        /// <summary>
        /// This overload explicitly loads the XML config file 
        /// </summary>
        public Dictionary<string, string> GetConfiguredVariableValues(Package package,
            string configurationSubsetName, string xmlConfigFileName)
        {
            LoadXmlDoc(xmlConfigFileName);
            
            ConfigureFromXmlStore(configurationSubsetName, package);

            return GetConfiguredVariableValues(package, configurationSubsetName);
        }

        #endregion

        #endregion

        #region Private Worker Routines

        private void LoadXmlDoc(string fileName)
        {
            configDoc = new XmlDocument();
            configDoc.Load(fileName);

        }

        private void SetVariableValue(string variableName, string variableValue, 
            Package package)
        {
            try
            {
                if (package.Variables.Contains(variableName))
                {
                    switch (package.Variables[variableName].DataType)
                    {
                        case TypeCode.Boolean:
                            package.Variables[variableName].Value = (bool.Parse(variableValue));
                            break;
                        case TypeCode.Byte:
                            package.Variables[variableName].Value = (byte.Parse(variableValue));
                            break;
                        case TypeCode.Char:
                            package.Variables[variableName].Value = (char.Parse(variableValue));
                            break;
                        case TypeCode.DateTime:
                            package.Variables[variableName].Value = (DateTime.Parse(variableValue));
                            break;
                        case TypeCode.Decimal:
                            package.Variables[variableName].Value = (Decimal.Parse(variableValue));
                            break;
                        case TypeCode.Double:
                            package.Variables[variableName].Value = (double.Parse(variableValue));
                            break;
                        case TypeCode.Int16:
                            package.Variables[variableName].Value = (Int16.Parse(variableValue));
                            break;
                        case TypeCode.Int32:
                            package.Variables[variableName].Value = (Int32.Parse(variableValue));
                            break;
                        case TypeCode.Int64:
                            package.Variables[variableName].Value = (Int64.Parse(variableValue));
                            break;
                        case TypeCode.SByte:
                            package.Variables[variableName].Value = (SByte.Parse(variableValue));
                            break;
                        case TypeCode.Single:
                            package.Variables[variableName].Value = (float.Parse(variableValue));
                            break;
                        case TypeCode.String:
                            package.Variables[variableName].Value = variableValue;
                            break;
                        case TypeCode.UInt16:
                            package.Variables[variableName].Value = (UInt16.Parse(variableValue));
                            break;
                        case TypeCode.UInt32:
                            package.Variables[variableName].Value = (UInt32.Parse(variableValue));
                            break;
                        case TypeCode.UInt64:
                            package.Variables[variableName].Value = (UInt64.Parse(variableValue));
                            break;
                        default:
                            throw new ApplicationException(string.Format("Could not update variable \'{0}\' with data type \'{1}\' to assign value \'{2}\' in package \'{3}\'.", variableName, package.Variables[variableName].DataType, variableValue, package.Name));
                    }
                }
            }
            catch (Exception ex)
            {
                throw new ApplicationException(string.Format("Could not update variable \'{0}\' to assign value \'{1}\' in package \'{2}\'.", variableName, variableValue, package.Name), ex);
            }
        }


        private void GetSiteVariableValues(XmlElement configSubsetElement, string packageName, string currentSite, string projectName)
        {
            string configSubsetname = GetRequiredAttributeString(configSubsetElement, "name", packageName);
            //  Site Name
            bool isSubsetEnabled = IsEnabled(configSubsetElement);
            
            if (isSubsetEnabled)
            {
                foreach (XmlElement configItem in configSubsetElement.SelectNodes("configurationItem"))
                {
                    if ((IsPackageMatch(configItem, packageName) && IsProjectMatch(configItem, projectName, packageName)))
                    {
                        string variableName = GetRequiredAttributeString(configItem, "variableName", packageName);
                        string variableValue = GetRequiredElementString(configItem, "variableValue", packageName);
                        AddVariableToConfigDictionary(variableName, variableValue);
                    }
                }
            }
        }



        private bool IsPackageMatch(XmlElement configurationItemElement, string packageName)
        {
            try
            {
                string packageMatchType = GetOptionalString(configurationItemElement, "packageMatchType", "Include");
                string packageMatchPattern = GetOptionalString(configurationItemElement, "packageMatchPattern", "*");
                if ((packageMatchType.ToUpper() == "EXCLUDE"))
                {
                    return false;
                }
                else if (((packageMatchPattern == "*")
                            || (packageMatchPattern == packageName)))
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }


        private bool IsProjectMatch(XmlElement configurationItemElement, string projectName, string packageName)
        {
            try
            {
                string projectMatchType = GetOptionalString(configurationItemElement, "projectMatchType", "Include");
                string projectMatchPattern = GetOptionalString(configurationItemElement, "projectMatchPattern", "*");
                if ((projectMatchType.ToUpper() == "EXCLUDE"))
                {
                    return false;
                }
                else if (((projectMatchPattern == "*")
                            || (projectMatchPattern == projectName)))
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private bool IsEnabled(XmlElement element)
        {
            try
            {
                return bool.Parse(element.Attributes["enabled"].Value);
            }
            catch 
            {
                return true;
            }
        }

        private string GetOptionalString(XmlElement element, string attributeName, string defaultValue)
        {
            try
            {
                return (string.IsNullOrEmpty(element.Attributes[attributeName].Value) ? 
                    defaultValue : 
                    element.Attributes[attributeName].Value).ToString();
            }
            catch 
            {
                return defaultValue;
            }
        }

        private string GetRequiredElementString(XmlElement element, string childElementName, string packageName)
        {
            try
            {
                string varName = element.Attributes["variableName"].Value;
                string newValue = element.SelectSingleNode(childElementName).InnerText;
                return newValue;
            }
            catch (Exception ex)
            {
                throw new ApplicationException(string.Format("The attribute \'{0}\' is required, but was not provided.", childElementName), ex);
            }
        }

        private string GetRequiredAttributeString(XmlElement element, string attributeName, string packageName)
        {
            try
            {
                return element.Attributes[attributeName].Value;
            }
            catch (Exception ex)
            {
                throw new ApplicationException(string.Format("The attribute \'{0}\' is required, but was not provided.", attributeName), ex);
            }
        }


        private void AddVariableToConfigDictionary(string variableName, string variableValue)
        {
            if (((variableName == VAR_CURRENT_SITE)
                        || (variableName == VAR_CURRENT_PROC)))
            {
                //  These variables cannot be set through the config store without 
                //  breaking the package, due to SSIS variable deadlocks
                return;
            }
            if (variablesToConfigure.ContainsKey(variableName))
            {
                //  The variable is already in the collection - update its value
                variablesToConfigure[variableName] = variableValue;
            }
            else
            {
                //  The variable is not yet in the collection - add it
                variablesToConfigure.Add(variableName, variableValue);
            }
        }



        #endregion 

    }
}
